package com.bitgate.util.service.protocol.http.handler;

import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.TimeZone;

import com.bitgate.util.bytes.ByteString;
import com.bitgate.util.service.protocol.http.HttpClientContext;
import com.bitgate.util.service.protocol.http.store.CookieStore;
import com.bitgate.util.uom.TimeUnit;

import static com.bitgate.util.debug.Debug.debug;

/**
 * This is a helper class that is used with the HTTP service to help with header generation.  It is strictly used to
 * create headers for the output of HTTP requests, and for use with HEAD requests as well.
 * 
 * @author kenji
 * @since 26-Aug-2007
 */
public class HttpHeaders
{
	private static DateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss z");
	
	/** This is the type of result to return. */
	public enum HeaderResult {
		/** Indicates a request from the client should continue with its request. */
		HTTP_CONTINUE						(100, "Continue"),
		
		/** Indicates the server understands and is wiling to comply with the clients request via the Upgrade header field. */
		HTTP_SWITCH_PROTOCOLS				(101, "Switching Protocols"),
		
		/** Indicates an HTTP OK response. */
		HTTP_OK								(200, "OK"),
		
		/**
		 * Indicates a request has been fulfilled and resulted in a new resource being created.  The URI response should
		 * contain an entity containing a list of resource characteristics and locations fom which the user or user agent can
		 * choose the one most appropriate, with the media type specified by the content-type header field.
		 * The server <b>MUST</b> create the content/resource before returning 201.
		 */
		HTTP_CREATED						(201, "Created"),
		
		/** Indicates the request has been accepted for processing but the processing has not been completed. */
		HTTP_ACCEPTED						(202, "Accepted"),
		
		/** Indicates the returned information is from a third party, not from the requested server. */
		HTTP_INFORMATION					(203, "Non-Authoritative Information"),
		
		/** Indicates the server fulfill the request but does not need to return an entity-body. */
		HTTP_NO_CONTENT						(204, "No Content"),
		
		/** 
		 * Indicates the server fulfilled the request, and user agent <i>should</i> reset the document view which caused the
		 * request to be sent.
		 */
		HTTP_RESET_CONTENT					(205, "Reset Content"),
		
		/**
		 * Indicates a continuation from a previous GET that was interrupted.  The response MUST include a Content-Range field,
		 * Date, ETag or Content-Location, and Expires, Cache-Control, and/or Vary if the field-value might differ from that
		 * sent in any previous response for the same variant.
		 */
		HTTP_PARTIAL_CONTENT				(206, "Partial Content"),
		
		/** Indicates a list of multiple locations for the requested content, only available in a HEAD request. */
		HTTP_MULTI_CHOICE					(300, "Multiple Choices"),
		
		/** Indicates a page has been assigned a new permanent URI, and that URI should be used for future requests. */
		HTTP_MOVED_PERM						(301, "Moved Permanently"),
		
		/** Indicates an HTTP Moved Temporarily response. */
		HTTP_MOVED							(302, "Moved Temporarily"),
		
		/**
		 * Indicates the response to the request can be found under a different URI and should be retrieved using a GET
		 * method on that resource.
		 */
		HTTP_SEE_OTHER						(303, "See Other"),
		
		/** Indicates the requested page has not been modified since the last request date. */
		HTTP_NOT_MODIFIED					(304, "Not Modified"),
		
		/** Indicates the requested resource <b>MUST</b> be accessed using a proxy given by the Location field. */
		HTTP_USE_PROXY						(305, "Use Proxy"),		
		
		/** Indicates the client made a request that was not recognized by the server. */
		HTTP_BAD_REQUEST					(400, "Bad Request"),
		
		/** Indicates the requested resource could not be retrieved without proper WWW-Authenticate headers. */
		HTTP_UNAUTHORIZED					(401, "Unauthorized"),
		
		/** Defined in the HTTP specification, but not specified as to what it does.  Reserved for future use. */
		HTTP_PAYMENT_REQUIRED				(402, "Payment Required"),
		
		/** Indicates the request made was to content that the server is refusing to return. */
		HTTP_FORBIDDEN						(403, "Forbidden"),
		
		/** Indicates an HTTP Not Found response. */
		HTTP_NOT_FOUND						(404, "Not Found"),
		
		/** Indicates a requested URI could not be accessed using the specified request method. */
		HTTP_METHOD_NOT_ALLOWED				(405, "Method Not Allowed"),
		
		/**
		 * Indicates the resource identified by the request is only capable of generating response entities which have
		 * content characteristics not acceptable according to the accept headers sent in the request.
		 */
		HTTP_NOT_ACCEPTABLE					(406, "Not Acceptable"),
		
		/** Indicates the PROXY request being made must send a Proxy-Authenticate header. */
		HTTP_PROXY_AUTH_REQUIRED			(407, "Proxy Authentication Required"),
		
		/** Indicates the client did not produce a requiest within the time that the server was prepared to wait. */
		HTTP_REQUEST_TIMEOUT				(408, "Request Timeout"),
		
		/** Indicates the request could not be completed due to a conflict with the state of the resource. */
		HTTP_CONFLICT						(409, "Conflict"),
		
		/** Indicates the resource is no longer available at the server, and no forwarding address is known. */
		HTTP_GONE							(410, "Gone"),
		
		/** Indicates the server refuses to accept the request without a Content-Length header. */
		HTTP_LENGTH_REQUIRED				(411, "Length Required"),
		
		/**
		 * Indicates a precondition given in one or more of the request-header fields evaluated to false when it was tested
		 * on the server.
		 */
		HTTP_PRECONDITION_FAILED			(412, "Precondition Failed"),
		
		/**
		 * Indicates the server is refusing to process a request because the request entity is larger than the server is
		 * willing or able to process.
		 */
		HTTP_REQUEST_ENTITY_TOO_LARGE		(413, "Request Entity Too Large"),
		
		/**
		 * Indicates the server is refusing to service the request because the Request-URI is longer than the server is
		 * willing to interpret.
		 */
		HTTP_REQUEST_URI_TOO_LONG			(414, "Request-URI Too Long"),
		
		/**
		 * Indicates the server is refusing to service the request because the entity of the request is in a format not
		 * supported by the requested resource for the requested method.
		 */
		HTTP_UNSUPPORTED_MEDIA_TYPE			(415, "Unsupported Media Type"),
		
		/** Indicates the request included in a range request-header field overlaps an if-range; rarely used. */
		HTTP_REQUEST_RANGE_NOT_SATISFIABLE	(416, "Requested Range Not Satisfiable"),
		
		/** Indicates the expectation given in an Expect request-header could not be met by this server. */
		HTTP_EXPECTATION_FAILED				(417, "Expectation Failed"),
		
		/** Indicates an internal server error occurred. */
		HTTP_INTERNAL_ERROR					(500, "Internal Server Error"),
		
		/** Indicates the server does not support the functionality required to fulfill the request. */
		HTTP_NOT_IMPLEMENTED				(501, "Not Implemented"),
		
		/**
		 * Indicates the server received a bad response from the requesting upstream server, and could not fulfill the
		 * request.
		 */ 
		HTTP_BAD_GATEWAY					(502, "Bad Gateway"),
		
		/** Indicates the server is currently unable to handle the request due to a temporary overloading or maintenance state. */
		HTTP_SERVICE_UNAVAILABLE			(503, "Service Unavailable"),
		
		/** Indicates the server did not receive a timely response from the upstream server. */
		HTTP_GATEWAY_TIMEOUT				(504, "Gateway Timeout"),
		
		/**
		 * Indicates the server does not support, or refuses to support, the HTTP protocol version that was used in the
		 * request message.
		 */
		HTTP_VERSION_NOT_SUPPORTED			(505, "HTTP Version Not Supported");
		
		private final int httpResult;
		private final String httpResultMessage;
		
		private HeaderResult(int httpResult, String httpResultMessage)
		{
			this.httpResult = httpResult;
			this.httpResultMessage = httpResultMessage;
		}
		
		/**
		 * Returns the numeric result of the message.
		 * 
		 * @return <code>int</code> containing the result.
		 */
		public int getResult()
		{
			return this.httpResult;
		}
		
		/**
		 * Returns the text representation of the message.
		 * 
		 * @return <code>String</code> containing the result.
		 */
		public String getResultMessage()
		{
			return this.httpResultMessage;
		}
		
		/**
		 * Generates a string representation of the entire HTTP response.
		 * 
		 * @return <code>String</code> containing the entire HTTP response.
		 */
		@Override
		public String toString()
		{
			return "" + getResult() + " " + getResultMessage();
		}
	}
	
	private HttpHeaders()
	{
		throw new RuntimeException("Cannot override this class.");
	}
	
	private static String getModificationDate(File file)
	{
		long lastModifiedTime = file.lastModified();
		Calendar cal = Calendar.getInstance();
		
		cal.setTimeZone(TimeZone.getTimeZone("GMT"));
		cal.setTime(new Date(lastModifiedTime));
		
		return httpDateFormat.format(cal.getTime());
	}
	
	private static String getExpireDate(String pageTimeout) 
	{
		long expireTime = System.currentTimeMillis();
		Calendar cal = Calendar.getInstance();
		
		if (pageTimeout != null) {
			expireTime += TimeUnit.DEFAULT.parse(pageTimeout);
		}
		
		cal.setTimeZone(TimeZone.getTimeZone("GMT"));
		cal.setTime(new Date(expireTime));
		
		return httpDateFormat.format(cal.getTime());
	}
	
	/**
	 * Creates a header for the requested file or HTTP transaction.
	 * 
	 * @param result The <code>HeaderResult</code> to generate.
	 * @param hcContext The <code>HttpClientContext</code> currently active for this request.
	 * @param length The length of the resulting response body.
	 * @param location The location to redirect the user to (only used if not <code>null</code>.)
	 * @param file The file object to use to get Last-Modified time.
	 * @return <code>StringBuffer</code> containing the appropriate headers to return to the client.
	 */
	public static StringBuffer createHeader(HeaderResult result, HttpClientContext hcContext, int length, String location, File file,
			String headers[])
	{
		StringBuffer sBuffer = new StringBuffer();
		String pageTimeout = hcContext.getProperty("cache.page-timeout");
		
		sBuffer.append("HTTP/1.1 ");
		sBuffer.append(result.toString());
		sBuffer.append("\r\n");
		sBuffer.append("Content-Type: ");
		sBuffer.append(hcContext.getMimeType());
		sBuffer.append("\r\n");
		sBuffer.append("Server: BSSE/Beta\r\n");
		
		if (location != null) {
			sBuffer.append("Location: ");
			sBuffer.append(location);
			sBuffer.append("\r\n");
		}
		
		sBuffer.append("Content-Length: ");
		sBuffer.append(Integer.toString(length));
		sBuffer.append("\r\n");
		
		if (file != null) {
			sBuffer.append("Last-Modified: ");
			sBuffer.append(getModificationDate(file));
			sBuffer.append("\r\n");
		}
		
		Set<String> outgoingCookieNames = hcContext.getOutgoingCookieSet();
		
		for(String outgoingCookie : outgoingCookieNames) {
			CookieStore cStore = hcContext.getOutgoingCookie(outgoingCookie);
			
			sBuffer.append("Set-Cookie: ")
				   .append(outgoingCookie)
				   .append('=')
				   .append(cStore.getValue());
			
			if (cStore.getExpiration() != null) {
				sBuffer.append("; expires=")
				   	   .append(cStore.getExpiration());
			}
			
			if (cStore.getPath() != null) {
				sBuffer.append("; path=")
					   .append(cStore.getPath());
			}
			
			if (cStore.getDomain() != null) {
				sBuffer.append("; domain=")
					   .append(cStore.getDomain());
			}
			
			sBuffer.append("\r\n");
			
			debug("Sending cookie name=" + outgoingCookie + " value=" + cStore.getValue());
		}
		
		if (pageTimeout != null) {
			sBuffer.append("Expires: ");
			sBuffer.append(getExpireDate(pageTimeout));
			sBuffer.append("\r\n");
		}
		
		// Keep-alive support here.
		if (location == null) {
			if (hcContext.getHeader(new ByteString("connection")) != null &&
				hcContext.getHeader(new ByteString("connection")).toString().toLowerCase().indexOf("keep-alive") != -1) {
				sBuffer.append("Connection: Keep-Alive\r\n");
			} else {
				sBuffer.append("Connection: Close\r\n");
			}
		} else {
			sBuffer.append("Connection: Close\r\n");
		}
		
		if (headers != null) {
			for(String header : headers) {
				sBuffer.append(header);
				sBuffer.append("\r\n");
			}
		}
		
		sBuffer.append("\r\n");
		
		return sBuffer;
	}
}